﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Collections;
using System.Drawing.Text;

namespace PassThePuzzle
{
    public partial class Gameform : Form
    {
        private FMOD.System _fModSystem = null;
        private FMOD.Channel _fModChannel = null;
        private FMOD.Sound _backgroundMusic = null;
        private FMOD.Sound _bangSound = null;

        private Random _rnd = new Random();

        private const string fontFamily = "Tahoma";
        private Font fontMessage = new Font(fontFamily, 24, FontStyle.Regular);

        private enum GameState
        {
            Uninitialised, Idle, IdleScores, Solving,
            Fadein, Playing, Paused, Boom, Scores, AddScoresKey, Solutions
        };
        private GameState _gameState = GameState.Uninitialised;

        private Settings _settings;
        private string[] _categoryMembers;
        private string _puzzle;
        private bool _attemptSolve;
        private string[] _solutions;
        private string _header = null;

        private int _windowHeight;
        private int _windowWidth;

        public Gameform(FMOD.System fModSystem)
        {
            InitializeComponent();
            _fModSystem = fModSystem;
            _fModSystem.createSound("ptp_bang.ogg", FMOD.MODE.LOOP_OFF, ref _bangSound);
            _fModSystem.createSound("ptp_tick.ogg", FMOD.MODE.LOOP_NORMAL, ref _backgroundMusic);
            _backgroundMusic.setLoopPoints(132300, FMOD.TIMEUNIT.PCM, 220500, FMOD.TIMEUNIT.PCM);
            pnlMain.Visible = true;
            _gameState = GameState.Idle;
        }

        private void startFModSound(FMOD.Sound snd, float volume, ref FMOD.Channel chn)
        {
            FMOD.CHANNELINDEX chIndex = FMOD.CHANNELINDEX.FREE;
            if (chn != null)
            {
                chn.stop();
                chIndex = FMOD.CHANNELINDEX.REUSE;
            }
            _fModSystem.playSound(chIndex, snd, true, ref chn);
            chn.setVolume(volume);
            chn.setPaused(false);
        }

        private void gameform_KeyPress(object sender, KeyEventArgs e)
        {
            bool eHandled = false;
            if (e.KeyCode == Keys.Escape)
            {
                if (_gameState != GameState.AddScoresKey)
                {
                    closeGameform();
                    eHandled = true;
                }
            }
            else if (e.KeyCode == Keys.Tab)
            {
                AnswerChecker answerChecker = new AnswerChecker(_categoryMembers);
                answerChecker.Show();
                switch (_gameState)
                {
                    case GameState.Playing:
                        pauseGame();
                        break;
                }
                eHandled = true;
            }
            else if (e.KeyCode == Keys.Enter)
            {
                switch (_gameState)
                {
                    case GameState.Playing:
                    case GameState.Paused:
                        boom(sender, new EventArgs());
                        eHandled = true;
                        break;
                    case GameState.Idle:
                    case GameState.IdleScores:
                    case GameState.Boom:
                    case GameState.Scores:
                    case GameState.Solutions:
                        startGame();
                        eHandled = true;
                        break;
                }
            }
            else if (e.KeyCode == Keys.Space)
            {
                switch (_gameState)
                {
                    case GameState.Playing:
                        pauseGame();
                        eHandled = true;
                        break;
                    case GameState.Paused:
                        unpauseGame();
                        eHandled = true;
                        break;
                    case GameState.Boom:
                    case GameState.Scores:
                        if (_attemptSolve) { showSolutions(); }
                        eHandled = true;
                        break;
                    case GameState.Solutions:
                        if (_settings.keepScores) { showScores(); }
                        eHandled = true;
                        break;
                }
            }
            e.Handled = eHandled;
        }

        /// <summary>
        /// Starts the game using the specified settings.
        /// </summary>
        /// <param name="settings">Settings object to contain the settings for the game.</param>
        public void StartGame(Settings settings, string categoryNames, string[] categoryMembers)
        {
            _windowHeight = ClientSize.Height;
            _windowWidth = ClientSize.Width;
            _categoryMembers = categoryMembers;
            _settings = settings;
            Utils.SwapIfNeeded(ref _settings.puzzleSizeMin, ref _settings.puzzleSizeMax);
            Utils.SwapIfNeeded(ref _settings.roundTimeMin, ref _settings.roundTimeMax);
            Utils.SwapIfNeeded(ref _settings.solutionCountMin, ref _settings.solutionCountMax);
            _header =
              (_settings.lettersBeginWith ? ", Starts with shown letters" : "") +
              (_settings.lettersEndWith ? ", Ends with shown letters" : "") +
              (_settings.lettersConsecutive ? ", Consecutive letters" : "") +
              (_settings.lettersGivenOrder ? ", Given order" : "");
            if (_header != "") { _header = _header.Substring(2) + "\n"; }
            _header += (categoryNames != null && categoryNames.Length > 0 ? categoryNames : "No category");
            _attemptSolve = (categoryMembers != null && categoryMembers.Length > 0);

            if (_settings.keepScores)
            {
                updateScores();
                showScores();
                _gameState = GameState.IdleScores;
                pnlMain.Refresh();
            }
            else
            {
                startGame();
            }
        }

        private void updateScores()
        {
            listViewScores.Items.Clear();
            if (_settings.scores != null)
            {
                foreach (var score in _settings.scores)
                {
                    listViewScores.Items.Add(new scoreListItem(score));
                }
            }
        }

        private void startGame()
        {
            labelHeading.Visible = false;
            listViewSolutions.Visible = false;
            listViewScores.Visible = false;
            pictureBoxBoom.Visible = false;
            pictureBoxBoom.Image = null;

            // Set puzzle
            int solutionCount = 0;
            int solutionCountMin = _settings.solutionCountMin;
            int solutionCountMax = _settings.solutionCountMax;
            int timeoutSeconds = 5;
            DateTime d = DateTime.Now;
            do
            {
                _puzzle = "";
                int puzzleSize = _rnd.Next(_settings.puzzleSizeMin, _settings.puzzleSizeMax + 1);
                for (int i = 0; i < puzzleSize; i++)
                {
                    _puzzle += _settings.letters[_rnd.Next(_settings.letters.Length)];
                }

                if (_attemptSolve)
                {
                    _gameState = GameState.Solving;
                    pnlMain.Refresh();
                    // Get solution
                    solvePuzzle();
                    solutionCount = _solutions.Count();
                }
            } while (DateTime.Now - d < TimeSpan.FromSeconds(timeoutSeconds)
              && _attemptSolve && (solutionCount < solutionCountMin || solutionCount > solutionCountMax));
            if (_attemptSolve)
            {
                if (solutionCount < solutionCountMin || solutionCount > solutionCountMax)
                {
                    MessageBox.Show(timeoutSeconds +
                      " seconds of randomly generating puzzles has not found one with " +
                      solutionCountMin + " to " +
                      solutionCountMax + " solution" + (solutionCountMax == 1 ? "" : "s") +
                      ", so it is likely there are no reasonable puzzles with your specified criteria.");
                    closeGameform();
                    return;
                }
            }

            // Start game
            timerBomb.Enabled = false;
            timerLetters.Enabled = false;
            startFModSound(_backgroundMusic, 1, ref _fModChannel);
            timerLetters.Enabled = true;

            _gameState = GameState.Fadein;
            pnlMain.Refresh();
        }


        private void solvePuzzle()
        {
            if (_categoryMembers.Length > 0)
            {
                string regexp;
                if (_settings.lettersGivenOrder)
                {
                    if (_settings.lettersConsecutive)
                    {
                        // given order and consecutive
                        regexp = _puzzle;
                    }
                    else
                    {
                        // given order and not necessarily consecutive
                        regexp = string.Join(".*", _puzzle.Select(c => c.ToString()).ToArray());
                    }
                }
                else
                {
                    if (_settings.lettersConsecutive)
                    {
                        // any order and consecutive
                        regexp = string.Join("|", _puzzle.Permutations().Select(p => new string(p.ToArray())).ToArray());
                    }
                    else
                    {
                        // any order and not necessarily consecutive
                        regexp = string.Join("|", _puzzle.Permutations().Select(p => string.Join(".*", p.Select(c => c.ToString()).ToArray())).ToArray());
                    }
                }
                _solutions = MatchLines(_categoryMembers,
                  (_settings.lettersBeginWith ? "^(" : "(") + regexp +
                  (_settings.lettersEndWith ? ")$" : ")")).Distinct().ToArray();
            }
        }

        public static IEnumerable<string> MatchLines(string[] lines, string regexp)
        {
            int index;
            foreach (string line in lines)
            {
                index = line.IndexOf('\t');
                if (index < 0)
                {
                    // only one string
                    if (Regex.IsMatch(Regex.Replace(line, "\\s", ""), regexp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
                        yield return line;
                }
                else
                {
                    // separate match and display strings
                    if (Regex.IsMatch(Regex.Replace(line.Substring(0, index), "\\s", ""), regexp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
                        yield return line.Substring(index + 1);
                }
            }
        }


        // PLAYING ROUND

        private void showPuzzle(object sender, EventArgs e)
        {
            timerLetters.Enabled = false;
            timerBomb.Interval = _rnd.Next(_settings.roundTimeMin * 1000, (_settings.roundTimeMax + 1) * 1000);
            timerBomb.Enabled = true;
            animationTimer.Enabled = true;
            _hourHandSpeed = (float) (-_rnd.NextDouble() * 0.05 - 0.1);
            _minuteHandSpeed = (float) (_rnd.NextDouble() * 0.05 + 0.1);
            _secondHandSpeed = (float) (_rnd.NextDouble() * 0.2 + 0.2);
            _gameState = GameState.Playing;
            pnlMain.Refresh();
        }


        // PAUSE

        private void pauseGame()
        {
            if (_fModChannel != null)
                _fModChannel.setPaused(true);
            timerBomb.Enabled = false;
            animationTimer.Enabled = false;
            _gameState = GameState.Paused;
            pnlMain.Refresh();
        }

        private void unpauseGame()
        {
            if (_fModChannel != null)
                _fModChannel.setPaused(false);
            timerBomb.Interval = _rnd.Next(0, Math.Max(_settings.roundTimeMin, _settings.roundTimeMax) * 500);
            timerBomb.Enabled = true;
            animationTimer.Enabled = true;
            _gameState = GameState.Playing;
            pnlMain.Refresh();
        }


        // END OF ROUND

        private void boom(object sender, EventArgs e)
        {
            startFModSound(_bangSound, 1, ref _fModChannel);
            timerBomb.Enabled = false;
            animationTimer.Enabled = false;
            int lineHeight = (int) Graphics.FromHwnd(Handle).MeasureString("Wg", fontMessage).Height;
            pictureBoxBoom.Location = new Point(0, lineHeight * 2);
            pictureBoxBoom.Size = new Size(_windowWidth, _windowHeight - lineHeight * 4);
            pictureBoxBoom.Visible = true;
            pictureBoxBoom.Image = Image.FromFile("ptp_boom.gif");
            _gameState = GameState.Boom;
            pnlMain.Refresh();
            if (_settings.keepScores) { showScores(); }
        }


        // SCORES

        private void showScores()
        {
            listViewScores.Visible = true;
            listViewScores.Focus();
            labelHeading.Visible = true;
            labelHeading.Text = "Scores";
            _gameState = GameState.Scores;
            pnlMain.Refresh();
            listViewSolutions.Visible = false;
        }

        private void listViewScores_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Insert)
            {
                // Add player
                showAddScoresKey();
            }
            else
            {
                foreach (scoreListItem item in listViewScores.SelectedItems)
                {
                    if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right)
                    {
                        // Increment/decrement scores
                        item.Score.value += (e.KeyCode == Keys.Left ? -1 : 1);
                        listViewScores.RedrawItems(0, listViewScores.Items.Count - 1, false);
                        e.Handled = true;
                    }
                    else if (e.KeyCode == Keys.Delete)
                    {
                        // Remove 1 player
                        if (MessageBox.Show("Remove \"" + item.Score.name + "\" from the list?", "Remove player?", MessageBoxButtons.YesNo) == DialogResult.Yes)
                        {
                            _settings.scores.Remove(item.Score);
                            listViewScores.Items.Remove(item);
                        }
                        e.Handled = true;
                    }
                }
            }
        }

        private void showAddScoresKey()
        {
            txtAddScoresKey.Visible = true;
            txtAddScoresKey.Text = "Enter a new name";
            txtAddScoresKey.SelectAll();
            txtAddScoresKey.Focus();
            _gameState = GameState.AddScoresKey;
            pnlMain.Refresh();
        }

        private void txtAddScoresKey_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Enter || e.KeyCode == Keys.Escape)
            {
                if (e.KeyCode == Keys.Enter)
                {
                    var s = _settings.AddScores(txtAddScoresKey.Text);
                    updateScores();
                    foreach (scoreListItem item in listViewScores.Items)
                        if (item.Score == s)
                            item.Selected = true;
                }
                txtAddScoresKey.Visible = false;
                showScores();
                e.Handled = true;
            }
        }


        // SOLUTIONS

        private void showSolutions()
        {
            labelHeading.Text = Utils.Pluralise(_solutions.Length, "solution") + " found for \"" + _puzzle + "\"";
            listViewSolutions.Clear();
            foreach (string solution in _solutions.ToArray())
                listViewSolutions.Items.Add(solution);
            listViewSolutions.Visible = true;
            listViewSolutions.Focus();
            labelHeading.Visible = true;
            _gameState = GameState.Solutions;
            pnlMain.Refresh();
            listViewScores.Visible = false;
        }


        // END GAME

        private void gameform_FormClosing(object sender, FormClosingEventArgs e)
        {
            closeGameform();
            e.Cancel = true;
            Hide();
        }

        private void closeGameform()
        {
            timerBomb.Enabled = false;
            timerLetters.Enabled = false;
            if (_fModChannel != null) { _fModChannel.stop(); }
            Hide();
            Program.settingsDlg.ShowScores();
            Program.settingsDlg.Show();
        }


        // PAINT

        private float _secondHand = 0;
        private float _minuteHand = 0;
        private float _hourHand = 0;
        private float _secondHandSpeed = 0;
        private float _minuteHandSpeed = 0;
        private float _hourHandSpeed = 0;

        private void paint(object sender, PaintEventArgs e)
        {
            if (_gameState == GameState.Playing || _gameState == GameState.Paused)
            {
                e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
                e.Graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
                drawHands(e.Graphics);

                if (_puzzle != null)
                {
                    for (int i = 0; i < 2; i++)
                    {
                        GraphicsPath p = new GraphicsPath();
                        p.AddString(_puzzle,
                          new FontFamily(fontFamily), 0, _windowWidth / (_puzzle.Length + 1),
                          new Point(_windowWidth / 2, _windowHeight / 2),
                          new StringFormat()
                          {
                              Alignment = StringAlignment.Center,
                              LineAlignment = StringAlignment.Center
                          });
                        if (i == 0)
                            p.Widen(new Pen(Color.Black, 8));
                        e.Graphics.FillPath(new SolidBrush(i == 0 ? Color.Black : (_gameState == GameState.Playing ? Color.White : Color.Navy)), p);
                    }
                    if (_settings.showSolutionCount && _attemptSolve && _solutions != null)
                    {
                        e.Graphics.DrawString(Utils.Pluralise(_solutions.Length, "solution") + " found",
                          new Font(fontFamily, 36, FontStyle.Bold),
                          new SolidBrush(Color.White),
                          _windowWidth / 2, _windowHeight * 4 / 5,
                          new StringFormat()
                          {
                              Alignment = StringAlignment.Center,
                              LineAlignment = StringAlignment.Center,
                          });
                    }
                    if (_gameState == GameState.Paused)
                    {
                        // Draw "PAUSED"
                        for (int i = 0; i < 2; i++)
                        {
                            GraphicsPath p = new GraphicsPath();
                            p.AddString("PAUSED", new FontFamily(fontFamily), 0, _windowWidth / 4,
                              new Point(_windowWidth / 2, _windowHeight / 2),
                              new StringFormat()
                              {
                                  Alignment = StringAlignment.Center,
                                  LineAlignment = StringAlignment.Center
                              });
                            if (i == 0)
                                p.Widen(new Pen(Color.Black, 8));
                            e.Graphics.FillPath(new SolidBrush(i == 0 ? Color.Black : Color.White), p);
                        }
                    }
                }
            }
        }

        private void drawHands(Graphics g)
        {
            float radius = (float) _windowHeight * 5 / 20;
            g.DrawLine(new Pen(Color.FromArgb(0, 32, 192), 25) { StartCap = LineCap.Round, EndCap = LineCap.ArrowAnchor }, _windowWidth / 2, _windowHeight / 2, _windowWidth / 2 + radius * (float) Math.Cos(_hourHand), _windowHeight / 2 + radius * (float) Math.Sin(_hourHand));
            radius = (float) _windowHeight * 3 / 10;
            g.DrawLine(new Pen(Color.FromArgb(0, 64, 224), 15) { StartCap = LineCap.Round, EndCap = LineCap.ArrowAnchor }, _windowWidth / 2, _windowHeight / 2, _windowWidth / 2 + radius * (float) Math.Cos(_minuteHand), _windowHeight / 2 + radius * (float) Math.Sin(_minuteHand));
            radius = (float) _windowHeight * 7 / 20;
            g.DrawLine(new Pen(Color.FromArgb(0, 96, 255), 10) { StartCap = LineCap.Round, EndCap = LineCap.ArrowAnchor }, _windowWidth / 2, _windowHeight / 2, _windowWidth / 2 + radius * (float) Math.Cos(_secondHand), _windowHeight / 2 + radius * (float) Math.Sin(_secondHand));
        }

        private void paintBuffer(object sender, PaintEventArgs e)
        {
            string footer = null;
            string footerEsc = "Esc to end game";

            e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
            e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
            e.Graphics.Clear(Color.Black);

            switch (_gameState)
            {
                case GameState.Idle:
                    footer = "Enter to start round";
                    break;
                case GameState.Solving:
                    e.Graphics.DrawString("Setting the puzzle...",
                      new Font(fontFamily, 48, FontStyle.Bold),
                      new SolidBrush(Color.Gray),
                      _windowWidth / 2, _windowHeight / 2,
                      new StringFormat()
                      {
                          Alignment = StringAlignment.Center,
                          LineAlignment = StringAlignment.Center
                      });
                    break;
                case GameState.Fadein:
                    drawClockFace(e.Graphics);
                    break;
                case GameState.Playing:
                    footer = "Space to pause, Enter to end round";
                    drawClockFace(e.Graphics);
                    break;
                case GameState.Paused:
                    footer = "Space to unpause, Enter to end round";
                    drawClockFace(e.Graphics);
                    break;
                case GameState.Boom:
                    footer = "Enter to start round";
                    if (_attemptSolve) { footer += ", Space to show solutions"; }
                    break;
                case GameState.IdleScores:
                case GameState.Scores:
                    footer = "\u2191/\u2193 to highlight a score, " +
                      "\u2190/\u2192/Del to lower/raise/remove highlighted score,\r\n" +
                      "Ins to add a player, Enter to start round";
                    if (_attemptSolve && _gameState == GameState.Scores)
                        footer += ", Space to show solutions";
                    break;
                case GameState.AddScoresKey:
                    footer = "Enter to accept player name";
                    footerEsc = "Esc to cancel addition of player";
                    break;
                case GameState.Solutions:
                    footer = "Enter to start round";
                    if (_settings.keepScores) { footer += ", Space to show scores"; }
                    break;
            }

            // Header and footer
            if (_header != null)
            {
                e.Graphics.DrawString(_header,
                  fontMessage,
                  new SolidBrush(Color.White),
                  _windowWidth / 2, 0,
                  new StringFormat()
                  {
                      Alignment = StringAlignment.Center,
                      LineAlignment = StringAlignment.Near
                  });
            }

            e.Graphics.DrawString("Press " + (footer != null ? footer + " or " : "") + footerEsc,
              fontMessage,
              new SolidBrush(Color.Gray),
              _windowWidth / 2, _windowHeight,
              new StringFormat()
              {
                  Alignment = StringAlignment.Center,
                  LineAlignment = StringAlignment.Far
              });
        }

        private void drawClockFace(Graphics g)
        {
            RectangleF rect = new RectangleF(_windowHeight / 10 + (_windowWidth - _windowHeight) / 2, _windowHeight / 10, _windowHeight * 4 / 5, _windowHeight * 4 / 5);
            g.FillEllipse(new SolidBrush(Color.Navy), rect);
            Pen thin = new Pen(Color.Blue, 5) { StartCap = LineCap.Round, EndCap = LineCap.Round };
            Pen thick = new Pen(Color.FromArgb(0, 64, 255), 10) { StartCap = LineCap.Round, EndCap = LineCap.Round };
            g.DrawEllipse(thin, rect);
            for (int i = 0; i < 360; i += 30)
                g.DrawLine((i % 90 == 0) ? thick : thin, (float) (_windowWidth / 2 + (_windowHeight * 15 / 40) * Math.Cos(i * Math.PI / 180)), (float) (_windowHeight / 2 + (_windowHeight * 15 / 40) * Math.Sin(i * Math.PI / 180)),
                    (float) (_windowWidth / 2 + (_windowHeight * 7 / 20) * Math.Cos(i * Math.PI / 180)), (float) (_windowHeight / 2 + (_windowHeight * 7 / 20) * Math.Sin(i * Math.PI / 180)));
        }

        private void listViewScores_Resize(object sender, EventArgs e)
        {
            listViewScores.Columns[0].Width = listViewScores.ClientSize.Width - listViewScores.Columns[1].Width;
        }

        private void listViewScores_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e)
        {
            if (e.ColumnIndex == 0)
                listViewScores.Columns[1].Width = listViewScores.ClientSize.Width - listViewScores.Columns[0].Width;
            else
                listViewScores.Columns[0].Width = listViewScores.ClientSize.Width - listViewScores.Columns[1].Width;
        }

        private class ScoreValueComparer : IComparer
        {
            private bool _reversed;
            public ScoreValueComparer(bool reversed) { _reversed = reversed; }
            public int Compare(object x, object y)
            {
                if (x is scoreListItem && y is scoreListItem)
                    if (_reversed)
                        return ((scoreListItem) y).Score.value.CompareTo(((scoreListItem) x).Score.value);
                    else
                        return ((scoreListItem) x).Score.value.CompareTo(((scoreListItem) y).Score.value);
                return 0;
            }
        }
        private class ScoreNameComparer : IComparer
        {
            private bool _reversed;
            public ScoreNameComparer(bool reversed) { _reversed = reversed; }
            public int Compare(object x, object y)
            {
                if (x is scoreListItem && y is scoreListItem)
                    if (_reversed)
                        return ((scoreListItem) y).Score.name.CompareTo(((scoreListItem) x).Score.name);
                    else
                        return ((scoreListItem) x).Score.name.CompareTo(((scoreListItem) y).Score.name);
                return 0;
            }
        }
        private int _lastSortColumn = 0;
        private void listViewScores_ColumnClick(object sender, ColumnClickEventArgs e)
        {
            if (e.Column == _lastSortColumn)
                listViewScores.Sorting = listViewScores.Sorting == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending;
            else
            {
                listViewScores.Sorting = SortOrder.Ascending;
                _lastSortColumn = e.Column;
            }
            listViewScores.ListViewItemSorter = e.Column == 0 ? (IComparer) new ScoreNameComparer(listViewScores.Sorting == SortOrder.Descending) : new ScoreValueComparer(listViewScores.Sorting == SortOrder.Descending);
            listViewScores.Sort();
        }

        private void animationTimer_Tick(object sender, EventArgs e)
        {
            _hourHand += _hourHandSpeed;
            _minuteHand += _minuteHandSpeed;
            _secondHand += _secondHandSpeed;
            pnlMain.Invalidate();
        }
    }
}
